package com.lhkj.ct.framework.auth.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.collect.Maps;
import com.lhkj.ct.base.cache.redis.RedisAccess;
import com.lhkj.ct.base.constants.CacheConstants;
import com.lhkj.ct.base.constants.ConfigConstants;
import com.lhkj.ct.base.constants.Constants;
import com.lhkj.ct.base.enums.GlobalStatus;
import com.lhkj.ct.base.enums.IamsServiceEnum;
import com.lhkj.ct.base.enums.PatSourceEnum;
import com.lhkj.ct.base.enums.ReturnStatus;
import com.lhkj.ct.base.exception.BusinessException;
import com.lhkj.ct.base.exception.ValidatedException;
import com.lhkj.ct.base.exception.user.BlackListException;
import com.lhkj.ct.base.exception.user.CaptchaException;
import com.lhkj.ct.base.exception.user.CaptchaExpireException;
import com.lhkj.ct.base.exception.user.UserNotExistsException;
import com.lhkj.ct.base.model.ro.ActionResult;
import com.lhkj.ct.base.utils.*;
import com.lhkj.ct.base.utils.http.HttpUtils;
import com.lhkj.ct.base.utils.uuid.UUID;
import com.lhkj.ct.framework.async.AsyncManager;
import com.lhkj.ct.framework.async.factory.AsyncFactory;
import com.lhkj.ct.framework.auth.dto.PatRegisterDto;
import com.lhkj.ct.framework.auth.mapstruct.RegisterConverter;
import com.lhkj.ct.framework.auth.ro.WxOAuth2UserInfo;
import com.lhkj.ct.framework.auth.service.AuthService;
import com.lhkj.ct.framework.shiro.token.AccessTokenAuthenticationToken;
import com.lhkj.ct.framework.shiro.token.UsernamePasswordAuthToken;
import com.lhkj.ct.meta.admin.service.SysConfigService;
import com.lhkj.ct.meta.modules.auth.common.enums.IdentType;
import com.lhkj.ct.meta.modules.auth.common.enums.UserType;
import com.lhkj.ct.meta.modules.auth.entity.TblUserIdentity;
import com.lhkj.ct.meta.modules.auth.service.UserIdentityService;
import com.lhkj.ct.meta.modules.patient.mapper.TblPatSourceMapper;
import com.lhkj.ct.meta.modules.patient.model.entity.TblPatCardRelation;
import com.lhkj.ct.meta.modules.patient.model.entity.TblPatSource;
import com.lhkj.ct.meta.modules.patient.model.entity.TblPatient;
import com.lhkj.ct.meta.modules.patient.service.PatCardRelationService;
import com.lhkj.ct.meta.modules.patient.service.PatientService;
import com.wf.captcha.ArithmeticCaptcha;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 登录服务层
 * </p>
 *
 * @author : jobob
 */
@Service
public class AuthServiceImpl implements AuthService {

    private static final Logger log = LoggerFactory.getLogger(AuthServiceImpl.class);

    @Resource
    private RedisAccess redisAccess;

    @Resource
    private SysConfigService sysConfigService;

    @Resource
    private PatientService patientService;

    @Resource
    private UserIdentityService userIdentityService;

    @Resource
    private TblPatSourceMapper patSourceMapper;

    @Resource
    private PatCardRelationService patCardRelationService;


    @Override
    public Map<String, Object> getCaptcha(HttpServletRequest request) {
        Map<String, Object> map = Maps.newHashMap();
        boolean captchaEnabled = sysConfigService.selectCaptchaEnabled();
        map.put("captchaEnabled", captchaEnabled);
        if (!captchaEnabled) {
            return map;
        }
        // 算术类型验证码(宽、高、运算位数)
        ArithmeticCaptcha captcha = new ArithmeticCaptcha(120, 48, 2);
        // 获取运算的结果
        String text = captcha.text();
        // 运算结果存入redis，设置10分钟有效期
        String key = UUID.randomUUID().toString();
        redisAccess.setCacheObject(key, text, 10 * 60, TimeUnit.SECONDS);
        map.put("verifyCodeKey", key);
        map.put("verifyCodeBase64", captcha.toBase64());
        return map;

    }

    @Override
    public String webSystemLogin(String data) {
        try {
            data = EncryptionUtil.decryptString(data, EncryptionUtil.privateKey);
        } catch (Exception e) {
            log.error("解密登录数据失败:", e);
            throw new BusinessException(ReturnStatus.SC_BAD_REQUEST, "登录参数解析异常！");
        }
        UsernamePasswordAuthToken token = JSONObject.parseObject(data, UsernamePasswordAuthToken.class);
        token.setNoPassword(false);
        token.setUserType(1);
        // 验证码校验
        validateCaptcha(token.getUsername(), token.getKaptcha(), token.getVerKey(), 1);
        // 校验登录参数
        validateLoginParams(token);
        return sessionId(token);
    }

    @Override
    public String webPlatformLogin(String data) {
        try {
            data = EncryptionUtil.decryptString(data, EncryptionUtil.privateKey);
        } catch (Exception e) {
            log.error("解密登录数据失败:", e);
            throw new BusinessException(ReturnStatus.SC_BAD_REQUEST, "登录参数解析异常！");
        }
        UsernamePasswordAuthToken token = JSONObject.parseObject(data, UsernamePasswordAuthToken.class);
        token.setNoPassword(false);
        token.setUserType(2);
        // 验证码校验
        validateCaptcha(token.getUsername(), token.getKaptcha(), token.getVerKey(), 2);
        // 校验登录参数
        validateLoginParams(token);
        return sessionId(token);
    }

    @Override
    public void wechatLogin(String code,  HttpServletResponse response) throws IOException {
        String appid = sysConfigService.selectConfigByKey(ConfigConstants.SYS_WECHAT_APPID);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");
        if (ShiroUtil.getSubject().isAuthenticated()) {
            response.getWriter().write(JSON.toJSONString(ActionResult.success(MessageUtils.message("user.login.success"), ShiroUtil.getSubject().getSession().getId())));
            return;
        }
        HashMap<String, Object> map = new HashMap<>();
        map.put("code", code);
        map.put("mpId", appid);
        // 服务器service上获取
         String jsonResult = HttpUtils.sendGet(IamsServiceEnum.USERINFO_URL.getUrl(), map);
         ActionResult<WxOAuth2UserInfo> actionResult = JSONObject.parseObject(jsonResult, new TypeReference<ActionResult<WxOAuth2UserInfo>>() {});
        if (!Objects.equals(actionResult.getCode(), ReturnStatus.SUCCESS.getCode())) {
            log.error("获取公众号用户信息失败：{}", actionResult.getMessage());
            throw new BusinessException(actionResult.getCode(), "系统异常，无法登录！");
        }
        String openId = actionResult.getData().getOpenid();
        log.debug("微信公众号openId：{}", openId);
        TblUserIdentity identity = userIdentityService.getByIdentity(UserType.PLATFORM, IdentType.WXP, openId);
        if (null == identity) {
            String hKey = UUID.randomUUID().toString();
            redisAccess.setCacheMapValue(CacheConstants.USER_IDENTITER_TOKEN, hKey, actionResult.getData());
            // 未注册，则跳转绑定页
             response.getWriter().write(JSON.toJSONString(ActionResult.fail(ReturnStatus.UNREGISTERED, hKey)));
             return;
        }
        // 执行登录
        AccessTokenAuthenticationToken token = new AccessTokenAuthenticationToken(UserType.PLATFORM, openId, IdentType.WXP);
        String sessionId = null;
        try {
            sessionId = h5SessionId(token);
        } catch (Exception e) {
            response.getWriter().write(JSON.toJSONString(ActionResult.fail(e.getMessage())));
            return;
        }
//            response.setHeader(ShiroSessionManager.HEADER_TOKEN_NAME, sessionId);
        response.getWriter().write(JSON.toJSONString(ActionResult.success(MessageUtils.message("user.login.success"), sessionId)));
    }

    @Override
    @Transactional
    public String register(PatRegisterDto registerDto) {
        if (registerDto.getPatOrgan() == null)
            registerDto.setPatOrgan(sysConfigService.selectConfigByKey("register.default.organ"));
        LambdaQueryWrapper<TblPatient> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(TblPatient::getPatIdentNo, registerDto.getPatIdentNo())
                .eq(TblPatient::getPatIdentType, registerDto.getPatIdentType())
                .last("limit 1");
        TblPatient patient = patientService.getBaseMapper().selectOne(queryWrapper);
        // 用户不存在则建档
        if (patient == null) {
            boolean registerEnabled = sysConfigService.selectRegisterEnabled();
            if (!registerEnabled) {
                throw new BusinessException(ReturnStatus.SC_UNAUTHORIZED, "系统已关闭自动注册功能，请联系系统管理员");
            }
            patient = RegisterConverter.INSTANCE.register(registerDto);
            patient.setStatus(GlobalStatus.NORMAL);
            patientService.insert(patient);
        }
        // 档案来源
        patSourceMapper.insertOnDuplicate(new TblPatSource(patient.getId(), PatSourceEnum.WX, null));

        /**
         * 根据注册信息里面的凭证，获取缓存的微信用户信息
         * {@link AuthServiceImpl#wechatLogin(String, HttpServletResponse)}
         */
        WxOAuth2UserInfo wxOAuth2UserInfo = redisAccess.getCacheMapValue(CacheConstants.USER_IDENTITER_TOKEN, registerDto.getIdentifier());
        if (wxOAuth2UserInfo == null) {
            throw new ValidatedException("登录凭证已失效，请重新进入");
        }
        String openid = wxOAuth2UserInfo.getOpenid();
        // 就诊卡绑定
        patCardRelationService.createDefaultCard(TblPatCardRelation.builder()
                .identifier(openid)
                .isDefault(true)
                .patId(patient.getId())
                // fixme 这里未对接医院系统，暂时用身份证
                .patCardNo(patient.getPatIdentNo())
                .build());
        final String userId = patient.getId();
        // 这里移除旧的绑定
        userIdentityService.remove(new LambdaQueryWrapper<TblUserIdentity>()
                .and(w -> w
                        // 如果绑定了多个微信，则移除旧的
//                        .eq(TblUserIdentity::getUserId, userId)
//                        .or()
                        .eq(TblUserIdentity::getIdentifier, openid))
                .eq(TblUserIdentity::getUserType, UserType.PLATFORM)
                .eq(TblUserIdentity::getIdentType, IdentType.WXP)
        );
        // 增加新的绑定
        userIdentityService.save(
                TblUserIdentity.builder()
                        .userId(userId)
                        .identifier(openid)
                        .nickName(wxOAuth2UserInfo.getNickname())
                        .userType(UserType.PLATFORM)
                        .identType(IdentType.WXP)
                        .build());
        AccessTokenAuthenticationToken token = new AccessTokenAuthenticationToken(UserType.PLATFORM, openid, IdentType.WXP);
        String h5SessionId = h5SessionId(token);
        redisAccess.hDel(CacheConstants.USER_IDENTITER_TOKEN, registerDto.getIdentifier());
        return h5SessionId;
    }


    @Override
    public void webLogout() {
        Subject subject = ShiroUtil.getSubject();
        subject.logout();
    }

    /**
     * 校验验证码
     *
     * @param username 用户名
     * @param code     验证码
     * @param uuid     唯一标识
     * @return 结果
     */
    public void validateCaptcha(String username, String code, String uuid, Integer userType) {
        boolean captchaEnabled = sysConfigService.selectCaptchaEnabled();
        if (captchaEnabled) {
            String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
            String captcha = redisAccess.getCacheObject(verifyKey);
            redisAccess.deleteObject(verifyKey);
            if (captcha == null) {
                AsyncManager.me().execute(AsyncFactory.recordLoginLog(username, Constants.LOGIN_FAIL, MessageUtils.message("user.captcha.expire"), userType));
                throw new CaptchaExpireException();
            }
            if (!code.equalsIgnoreCase(captcha)) {
                AsyncManager.me().execute(AsyncFactory.recordLoginLog(username, Constants.LOGIN_FAIL, MessageUtils.message("user.captcha.error"), userType));
                throw new CaptchaException();
            }
        }
    }


    /**
     * 校验登录参数
     */
    public void validateLoginParams(UsernamePasswordAuthToken token) {
        Asserts.assertFalse(StringUtils.isBlank(token.getUsername()), "账号不能为空！");
        Asserts.assertFalse(StringUtils.isBlank(token.getPassword()), "密码不能为空！");
        // 验证码校验
        validateCaptcha(token.getUsername(), token.getKaptcha(), token.getVerKey(), token.getUserType());
        // IP黑名单校验
        String blackStr = sysConfigService.selectConfigByKey(ConfigConstants.SYS_BLACK_LIST);
        if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) {
            AsyncManager.me().execute(AsyncFactory.recordLoginLog(token.getUsername(), Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));
            throw new BlackListException();
        }
    }

    public String sessionId(UsernamePasswordAuthToken token) {
        Subject subject = ShiroUtil.getSubject();
        try {
            subject.login(token);
        } catch (IncorrectCredentialsException | UnknownAccountException e) {
            AsyncManager.me().execute(AsyncFactory.recordLoginLog(token.getUsername(), Constants.LOGIN_FAIL, MessageUtils.message("user.not.exists"), token.getUserType()));
            throw new UserNotExistsException();
        } catch (DisabledAccountException e) {
            AsyncManager.me().execute(AsyncFactory.recordLoginLog(token.getUsername(), Constants.LOGIN_FAIL, MessageUtils.message("user.blocked"), token.getUserType()));
            throw new AuthenticationException(e);
        }
        AsyncManager.me().execute(AsyncFactory.recordLoginLog(
                token.getUsername(),
                Constants.LOGIN_SUCCESS,
                MessageUtils.message("user.login.success"),
                token.getUserType()));
        return subject.getSession(false).getId().toString();
    }

    public String h5SessionId(AccessTokenAuthenticationToken token) {
        Subject subject = ShiroUtil.getSubject();
        // 其他时间 单位毫秒,如下设置一周
        subject.getSession().setTimeout(7 * 24 * 60 * 60 * 1000L);
        subject.login(token);
        AsyncManager.me().execute(AsyncFactory.recordLoginLog(
                ShiroUtil.getAuthUserInfo().getLoginName(),
                Constants.LOGIN_SUCCESS,
                "登录成功",
                UserType.PLATFORM.getCode())
        );
        return subject.getSession().getId().toString();
    }

    private String getWechatUserInfoJsonByLocal(String code, String appid) {
        String accessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token";
        String params = "appid="+ appid +"&secret=c68dd2f38cb1a74824eb547cf689f2c8&code=" + code + "&grant_type=authorization_code";
        String jsonResultAccess = HttpUtils.sendGet(accessTokenUrl, params);
        AccessResult accessResult = JSONObject.parseObject(jsonResultAccess, new TypeReference<AccessResult>() { });
        System.out.println("accessResult = " + accessResult);
        return accessResult.pullWechatUserInfo();
    }

}

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
class AccessResult{

    private String access_token;

    private Integer expires_in;

    private String refresh_token;

    private String openid;

    private String scope;

    private String is_snapshotuser;

    private String unionid;

    public String pullWechatUserInfo() {
        String url = "https://api.weixin.qq.com/sns/userinfo";
        String params = "access_token=" + this.access_token + "&openid=" + this.openid + "&lang=zh_CN";
        return HttpUtils.sendGet(url, params);
    }

}
